import {
  IAssemblyConstraint,
  IAssemblyDocTreeNode,
  useCommandManager,
  VisibleBox3,
} from '@amcax/base';
import { applyMatrix4ArrayToObject } from '@amcax/renderer';
import { cmdConstrain } from '@app-cad-env/product/commands/cmd-constrain';
import { constraintMap } from '@app-cad-env/product/commands/cmd-constrain/constant';
import { PRODUCT_EVENT } from '@app-cad/common/Constants';
import { useTools } from '@app-cad/common/hooks/useTools';
import { DocumentContext } from '@app-cad/common/providers/documentProvider';
import { assemblyGetNodeTitleModified } from '@app-cad/common/utils/assembly-utils/AssemblyNodeTitleFuncs';
import { CSS2DLabelWithIcon } from '@app-cad/common/utils/CSS2DLabelWithIcon';
import { ctx2, userEvents } from '@app-cad/module';
import { allParts } from '@app-cad/pages/product-support/part-funcs';
import React, {
  Dispatch,
  FC,
  SetStateAction,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { Vector3 } from 'three-legacy';

import './ConstraintItem.scss';

interface ConstraintItemProps {
  popupNodeTitles: Set<string>; // from which nodes the dialog pops up
  constraintNode: IAssemblyDocTreeNode;
  fixedConstraints: IAssemblyConstraint[];
  selectedIds: string[];
  setSelectedIds: Dispatch<SetStateAction<string[]>>;
}

export const ConstraintItem: FC<ConstraintItemProps> = ({
  popupNodeTitles,
  constraintNode,
  fixedConstraints,
  selectedIds,
  setSelectedIds,
}) => {
  const { docTree } = useContext(DocumentContext);
  const { pickerAssembly } = useTools();
  const { invokeCmd } = useCommandManager();
  const [rerendered, setRerendered] = useState(0);

  useEffect(() => {
    const abortController = new AbortController();
    userEvents.on(
      PRODUCT_EVENT.PARTS_RERENDERED,
      () => setRerendered((prev) => prev + 1),
      abortController.signal,
    );

    return () => abortController.abort();
  }, []);

  const constraintItem = constraintNode.originalObj as IAssemblyConstraint;
  const constraint = constraintMap.find((c) => c.const === constraintItem.type);
  const { id, item, target } = constraintItem;
  const constraintObjectIds = useMemo(() => [item, target], [item, target]);
  const pickedObjects = useMemo(
    () =>
      constraintObjectIds.map((id) => {
        const pickedObj = pickerAssembly.generatePickObjByPickID(id);

        const part = allParts.get(id.split(':')[0]);
        if (part) {
          applyMatrix4ArrayToObject(pickedObj.assisObj, part.matrix4Array);
        }

        return pickedObj;
      }),
    [constraintObjectIds, rerendered],
  );
  const centers = useMemo(
    () =>
      pickedObjects.map(({ assisObj }) => {
        const box = new VisibleBox3();
        box.setFromObject(assisObj);
        const center = box.getCenter(new Vector3());

        return center;
      }),
    [pickedObjects],
  ) as [Vector3, Vector3];

  const selected = useMemo(() => selectedIds.includes(id), [selectedIds, id]);
  const labelRef = useRef<CSS2DLabelWithIcon | null>(null);

  useEffect(() => {
    const clearLabel = () => {
      pickedObjects.forEach(({ assisObj }) =>
        ctx2.cadScene.toolsGroup.remove(assisObj),
      );

      if (labelRef.current) {
        labelRef.current.destroy();
        labelRef.current = null;
      }

      ctx2.viewer.requestRender();
    };

    if (selected) {
      const label = new CSS2DLabelWithIcon({
        name: constraintItem.label,
        icon: constraint?.icon,
        centers,
      });
      pickedObjects.forEach(({ assisObj }) =>
        assisObj.addTo(ctx2.cadScene.toolsGroup),
      );
      label.addTo(ctx2.cadScene.toolsGroup);
      labelRef.current = label;

      ctx2.viewer.requestRender();
    } else {
      clearLabel();
    }

    return clearLabel;
  }, [selected, pickedObjects, rerendered]);

  const partChains = useMemo(() => {
    return constraintObjectIds.map((partId) => {
      const chain = [];
      let current = allParts.get(partId.split(':')[0]);
      while (current?.parent && current.parent !== docTree) {
        chain.unshift(current);
        current = current.parent;
      }
      if (current?.parent === docTree) {
        chain.unshift(current);
      }
      return chain;
    });
  }, [item, target, docTree]);

  const objectLabels = partChains.map((partChain) => {
    for (let i = 0; i < partChain.length; i++) {
      const title = assemblyGetNodeTitleModified(partChain[i]);
      if (popupNodeTitles.has(title)) {
        return title;
      }
    }

    return partChain.length > 0
      ? assemblyGetNodeTitleModified(partChain.at(-1)!)
      : '';
  });
  const bothFixed = partChains.every((chain) =>
    fixedConstraints.map(({ item }) => item).includes(chain[0]?.key),
  );
  const label = `${constraintItem.label} (${objectLabels[0]}，${objectLabels[1]})`;

  const handleSelect = (e: React.MouseEvent | React.KeyboardEvent) => {
    if (e.ctrlKey || e.metaKey || e.shiftKey) {
      // Toggle selection
      setSelectedIds((prev: string[]) =>
        prev.includes(id) ? prev.filter((sid) => sid !== id) : [...prev, id],
      );
    } else {
      setSelectedIds([id]);
    }
  };

  return (
    <div
      className={`constraint-item w-full p-2 flex items-center cursor-pointer select-none ${
        selected
          ? 'outline outline-1 outline-[#464681] bg-[#f2f2ff]'
          : 'hover:bg-[#f2f2ff]'
      } ${bothFixed ? 'text-gray-400' : ''}`}
      role='button'
      tabIndex={0}
      aria-pressed={selected}
      onClick={handleSelect}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          handleSelect(e);
        }
      }}
      onDoubleClick={async () => {
        await invokeCmd(cmdConstrain, constraintItem as any);
      }}
    >
      <span className={bothFixed ? 'constraint-icon-gray' : ''}>
        {constraint?.icon}
      </span>
      <span
        className={`ml-2 max-w-[800px] truncate`}
        title={label}
      >
        {label}
      </span>
    </div>
  );
};
